//@author A0096759E

package logic;

import org.joda.time.DateTimeConstants;
import org.joda.time.LocalDate;

import storage.Storage;
import applogger.AppLogger;

public class ScheduleSearcher {
	
	private static final String STRING_WHITESPACE = " ";
	private static final String SEARCH_BY_TYPE_FLOATING = "float";
	private static final String SEARCH_BY_TYPE_DEADLINE = "dead";
	private static final String SEARCH_BY_TYPE_TIMED = "timed";
	private static final String SEARCH_MESSAGE_KEYWORD = "Entries with search term \"%s\" in your Schedule:";
	private static final String SEARCH_MESSAGE_DATE = "Entries on \"%s\" in your Schedule:";
	private static final String SEARCH_MESSAGE_DATE_RANGE = "Entries from \"%s\" to \"%s\" in your Schedule:";
	private static final String SEARCH_MESSAGE_TYPE = "Entries with the type \"%s\" in your Schedule:";
	private static final String SEARCH_MESSAGE_TYPE_FLOATING = "Floating";
	private static final String SEARCH_MESSAGE_TYPE_DEADLINE = "Deadline";
	private static final String SEARCH_MESSAGE_TYPE_TIMED = "Timed";
	private static final String SEARCH_RESULTS_TEMPLATE = "\n\n%s. %s";
	
	private static final String ERROR_MSG_INVALID_SEARCH_COMMAND = "ERROR! The search command was not recognized. Please try again!";
	private static final String ERROR_MSG_SEARCH_RESULTS_NOT_FOUND = "No search results found. Please try again!";
	
	private static final int EVENT_TYPE_FLOATING = 0;
	private static final int EVENT_TYPE_DEADLINE = 1;
	private static final int EVENT_TYPE_TIMED = 2;
	private static final int DATE_TO_INT_DD = 0;
	private static final int DATE_TO_INT_MM = 2;
	private static final int DATE_TO_INT_YYYY = 4;
	private static final int SHIFT_LEFT_FOUR = 10000;
	private static final int SHIFT_LEFT_TWO = 100;
	
	private static final char DIGIT_MAX = '9';
	private static final char DIGIT_MIN = '0';
	
	
	private Storage eventsList;
	private ScheduleSorter sorter;

	/**
	 * Constructor for ScheduleSearcher. Takes in the current LifeShelf storage and a ScheduleSorter
	 * @param eventsList
	 */
	public ScheduleSearcher(Storage eventsList, ScheduleSorter sorter) {
		this.eventsList = eventsList;
		this.sorter = sorter;
	}

	/**
	 * Main search method. Breaks the user command down and checks whether it is valid. Then calls the
	 * next in line search command decoder
	 * @param userCommandDetails
	 * @return found items if successful as a string, else error message.
	 */
	public String searchEvents(String userCommandDetails) {
		String[] sortCommandDetails = userCommandDetails.split(STRING_WHITESPACE, 2);
		boolean needsToSort = checkSpecificSortCommand(sortCommandDetails[0]);
		sorter.sortEvents(sortCommandDetails[0]);
		if (needsToSort) {
			if (sortCommandDetails.length < 2) {
				return ERROR_MSG_INVALID_SEARCH_COMMAND;
			}
			return searchCommandDecoder(sortCommandDetails[1]);
		}
		return searchCommandDecoder(userCommandDetails);
	}

	/**
	 * Checks whether the incoming search string has any sorting flags.
	 * @param sortCommandDetails
	 * @return true if there is a flag.
	 */
	private boolean checkSpecificSortCommand(String sortCommandDetails) {
		if (sortCommandDetails.charAt(0) == '-') return true;
		return false;
	}

	/**
	 * Decodes the user command further and calls the respective types of search.
	 * @param userCommandDetails
	 * @return the data found as a string, or error message if nothing found.
	 */
	private String searchCommandDecoder(String userCommandDetails) {
		AppLogger.log("Decoding Search Command");
		String[] searchCommandDetails = userCommandDetails.split(STRING_WHITESPACE);
		if (searchCommandDetails.length == 2 && isDate(searchCommandDetails[0]) && isDate(searchCommandDetails[1])) {
			return searchEventsByDateRange(searchCommandDetails[0], searchCommandDetails[1]);
		} else if (searchCommandDetails.length == 1) {
			if (isDate(searchCommandDetails[0])) {
				return searchEventsByDate(searchCommandDetails[0]);
			} else {
				switch(searchCommandDetails[0]) {
				case SEARCH_BY_TYPE_FLOATING:
					return searchEventsByType(EVENT_TYPE_FLOATING);
				case SEARCH_BY_TYPE_DEADLINE:
					return searchEventsByType(EVENT_TYPE_DEADLINE);
				case SEARCH_BY_TYPE_TIMED:
					return searchEventsByType(EVENT_TYPE_TIMED);
				}
			}
		}
		return searchEventsByKeyword(userCommandDetails);
	}

	/**
	 * Searches for events with the input search term
	 * @param userCommandDetails - the search term
	 * @return found data if any as a string, or error message.
	 */
	private String searchEventsByKeyword(String userCommandDetails) {
		AppLogger.log("Searching Events By Keyword");
		int resultsFound = 0;
		StringBuilder searchResults = new StringBuilder();
		searchResults.append(String.format(SEARCH_MESSAGE_KEYWORD, userCommandDetails));
		for (int index = 0; index < eventsList.getSize(); index++) {
			if (eventsList.getEvent(index).getName().toLowerCase().contains(userCommandDetails.toLowerCase())) {
				searchResults.append(String.format(SEARCH_RESULTS_TEMPLATE, index+1,eventsList.getEvent(index).toString()));
				resultsFound++;
			}
		}
		if (resultsFound==0) {
			return ERROR_MSG_SEARCH_RESULTS_NOT_FOUND;
		}
		return searchResults.toString();
	}
	
	/**
	 * Searches for events with the input type
	 * @param eventTypeToSearch
	 * @return found data if any as a string, or error message.
	 */
	private String searchEventsByType(int eventTypeToSearch) {
		AppLogger.log("Searching Events By Type");
		int resultsFound = 0;
		StringBuilder searchResults = new StringBuilder();
		switch (eventTypeToSearch) {
		case EVENT_TYPE_FLOATING:
			searchResults.append(String.format(SEARCH_MESSAGE_TYPE, SEARCH_MESSAGE_TYPE_FLOATING));
			break;
		case EVENT_TYPE_DEADLINE:
			searchResults.append(String.format(SEARCH_MESSAGE_TYPE, SEARCH_MESSAGE_TYPE_DEADLINE));
			break;
		case EVENT_TYPE_TIMED:
			searchResults.append(String.format(SEARCH_MESSAGE_TYPE, SEARCH_MESSAGE_TYPE_TIMED));
			break;
		}
		for (int index = 0; index < eventsList.getSize(); index++) {
			if (eventsList.getEvent(index).getType() == eventTypeToSearch) {
				searchResults.append(String.format(SEARCH_RESULTS_TEMPLATE, index+1,eventsList.getEvent(index).toString()));
				resultsFound++;
			}
		}
		if (resultsFound==0) {
			return ERROR_MSG_SEARCH_RESULTS_NOT_FOUND;
		}
		return searchResults.toString();
	}

	/**
	 * Searches for events with the input date
	 * @param dateToSearch
	 * @return found data if any as a string, or error message.
	 */
	public String searchEventsByDate(String dateToSearch) {
		AppLogger.log("Searching Events By Date");
		int resultsFound = 0;
		StringBuilder searchResults = new StringBuilder();
		if(ifHasNameOfDay(dateToSearch))
			dateToSearch = getDateFromDay(dateToSearch);
		searchResults.append(String.format(SEARCH_MESSAGE_DATE, dateToSearch));
		for (int index = 0; index < eventsList.getSize(); index++) {
			if (eventsList.getEvent(index).getDate().equals(dateToSearch)) {
				searchResults.append(String.format(SEARCH_RESULTS_TEMPLATE, index+1,eventsList.getEvent(index).toString()));
				resultsFound++;
			}
		}
		if (resultsFound==0) {
			return ERROR_MSG_SEARCH_RESULTS_NOT_FOUND;
		}
		return searchResults.toString();
	}

	/**
	 * Searches for events with the input date range as two terms
	 * @param dateStart
	 * @param dateEnd
	 * @return found data if any as a string, or error message.
	 */
	private String searchEventsByDateRange(String dateStart, String dateEnd) {
		AppLogger.log("Searching Events By Date Range");
		int resultsFound = 0;
		StringBuilder searchResults = new StringBuilder();
		int dateStartInt = convertDateToInteger(dateStart);
		int dateEndInt = convertDateToInteger(dateEnd);
		searchResults.append(String.format(SEARCH_MESSAGE_DATE_RANGE, dateStart, dateEnd));
		for (int index = 0; index < eventsList.getSize(); index++) {
			int eventDateInt = convertDateToInteger(eventsList.getEvent(index).getDate());
			if (dateStartInt <= eventDateInt && eventDateInt <= dateEndInt) {
				searchResults.append(String.format(SEARCH_RESULTS_TEMPLATE, index+1,eventsList.getEvent(index).toString()));
				resultsFound++;
			}
		}
		if (resultsFound==0) {
			return ERROR_MSG_SEARCH_RESULTS_NOT_FOUND;
		}
		return searchResults.toString();
	}
	
	/**
	 * Converts a date into an understandable integer
	 * @param date
	 * @return date as integer
	 */
	private int convertDateToInteger(String date) {
		int day,month,year;
		day = Integer.parseInt(date.substring(DATE_TO_INT_DD,DATE_TO_INT_MM));
		month = Integer.parseInt(date.substring(DATE_TO_INT_MM,DATE_TO_INT_YYYY))*SHIFT_LEFT_TWO;
		year = Integer.parseInt(date.substring(DATE_TO_INT_YYYY))*SHIFT_LEFT_FOUR;
		return (day+month+year);
	}

	/**
	 * Checks if an input string is a valid date.
	 * @param stringToCheck
	 * @return true if it is a valid date
	 */
	private boolean isDate(String stringToCheck) {
		if (ifHasNameOfDay(stringToCheck))
			return true;
		if (stringToCheck.length() == 8) {
			for (int i = 0; i < 8; i ++) {
				if (!isDigit(stringToCheck.charAt(i))) return false;
			}
			return true;
		}
		return false;
	}

	/**
	 * Checks if the input char is a digit
	 * @param charAt
	 * @return true if char is a digit
	 */
	private boolean isDigit(char charAt) {
		if ((int)charAt < (int)DIGIT_MIN || (int)charAt > (int)DIGIT_MAX) return false;
		else return true;
	}
	private boolean ifHasNameOfDay(String commandsList) {
		return commandsList.toLowerCase().contains("monday")||commandsList.toLowerCase().contains("tuesday")||
				commandsList.toLowerCase().contains("wednesday")||commandsList.toLowerCase().contains("thursday")||
				commandsList.toLowerCase().contains("friday")||commandsList.toLowerCase().contains("saturday")||
				commandsList.toLowerCase().contains("sunday");
	}
	private String getDateFromDay(String check) {
		String date = "empty :D";
		LocalDate today = new LocalDate();
		if(check.toLowerCase().equals("monday")){
			today = calcNextMonday(today);
			date = today.toString("ddMMyyyy");
		}
		else if(check.toLowerCase().equals("tuesday")){
			today = calcNextTuesday(today);
			date = today.toString("ddMMyyyy");
		}
		else if(check.toLowerCase().equals("wednesday")){
			today = calcNextWednesday(today);
			date = today.toString("ddMMyyyy");
		}
		else if(check.toLowerCase().equals("thursday")){
			today = calcNextThursday(today);
			date = today.toString("ddMMyyyy");
		}
		else if(check.toLowerCase().equals("friday")){
			today = calcNextFriday(today);
			date = today.toString("ddMMyyyy");
		}
		else if(check.toLowerCase().equals("saturday")){
			today = calcNextSaturday(today);
			date = today.toString("ddMMyyyy");
		}
		else if(check.toLowerCase().equals("sunday")){
			today = calcNextSunday(today);
			date = today.toString("ddMMyyyy");
		}
		return date;
	}
	private static LocalDate calcNextMonday(LocalDate d) {
	    if (d.getDayOfWeek() >= DateTimeConstants.MONDAY) {
	        d = d.plusWeeks(1);
	    }
	    return d.withDayOfWeek(DateTimeConstants.MONDAY);
	}
	
	private static LocalDate calcNextTuesday(LocalDate d) {
	    if (d.getDayOfWeek() >= DateTimeConstants.TUESDAY) {
	        d = d.plusWeeks(1);
	    }
	    return d.withDayOfWeek(DateTimeConstants.TUESDAY);
	}private static LocalDate calcNextWednesday(LocalDate d) {
	    if (d.getDayOfWeek() >= DateTimeConstants.WEDNESDAY) {
	        d = d.plusWeeks(1);
	    }
	    return d.withDayOfWeek(DateTimeConstants.WEDNESDAY);
	}private static LocalDate calcNextThursday(LocalDate d) {
	    if (d.getDayOfWeek() >= DateTimeConstants.THURSDAY) {
	        d = d.plusWeeks(1);
	    }
	    return d.withDayOfWeek(DateTimeConstants.THURSDAY);
	}
	private static LocalDate calcNextFriday(LocalDate d) {
	    if (d.getDayOfWeek() >= DateTimeConstants.FRIDAY) {
	        d = d.plusWeeks(1);
	    }
	    return d.withDayOfWeek(DateTimeConstants.FRIDAY);
	}
	private static LocalDate calcNextSaturday(LocalDate d) {
	    if (d.getDayOfWeek() >= DateTimeConstants.SATURDAY) {
	        d = d.plusWeeks(1);
	    }
	    return d.withDayOfWeek(DateTimeConstants.SATURDAY);
	}
	private static LocalDate calcNextSunday(LocalDate d) {
	    if (d.getDayOfWeek() >= DateTimeConstants.SUNDAY) {
	        d = d.plusWeeks(1);
	    }
	    return d.withDayOfWeek(DateTimeConstants.SUNDAY);
	}
	
}
